// SPDX-License-Identifier: GPL-3.0-or-later

#include "router.h"
#include "client.h"
#include "client_socket.h"
#include "roomthread.h"
#include <qjsondocument.h>
#ifndef FK_CLIENT_ONLY
#include "server.h"
#include "serverplayer.h"
#endif
#include "util.h"

Router::Router(QObject *parent, ClientSocket *socket, RouterType type)
    : QObject(parent) {
  this->type = type;
  this->socket = nullptr;
  setSocket(socket);
  expectedReplyId = -1;
  replyTimeout = 0;
#ifndef FK_CLIENT_ONLY
  extraReplyReadySemaphore = nullptr;
#endif
}

Router::~Router() { abortRequest(); }

ClientSocket *Router::getSocket() const { return socket; }

void Router::setSocket(ClientSocket *socket) {
  if (this->socket != nullptr) {
    this->socket->disconnect(this);
    disconnect(this->socket);
    this->socket->deleteLater();
  }

  this->socket = nullptr;
  if (socket != nullptr) {
    connect(this, &Router::messageReady, socket, &ClientSocket::send);
    connect(socket, &ClientSocket::message_got, this, &Router::handlePacket);
    connect(socket, &ClientSocket::disconnected, this, &Router::abortRequest);
    socket->setParent(this);
    this->socket = socket;
  }
}

void Router::removeSocket() {
  socket->disconnect(this);
  socket = nullptr;
}

void Router::installAESKey(const QByteArray &key) {
  socket->installAESKey(key);
}

#ifndef FK_CLIENT_ONLY
void Router::setReplyReadySemaphore(QSemaphore *semaphore) {
  extraReplyReadySemaphore = semaphore;
}
#endif

void Router::request(int type, const QString &command, const QString &jsonData,
                     int timeout) {
#ifndef FK_CLIENT_ONLY
  // In case a request is called without a following waitForReply call
  if (replyReadySemaphore.available() > 0)
    replyReadySemaphore.acquire(replyReadySemaphore.available());

  static int requestId = 0;
  requestId++;

  replyMutex.lock();
  expectedReplyId = requestId;
  replyTimeout = timeout;
  requestStartTime = QDateTime::currentDateTime();
  m_reply = "__notready";
  replyMutex.unlock();

  QJsonArray body;
  body << requestId;
  body << type;
  body << command;
  body << jsonData;
  body << timeout;

  emit messageReady(JsonArray2Bytes(body));
#endif
}

void Router::reply(int type, const QString &command, const QString &jsonData) {
  QJsonArray body;
  body << this->requestId;
  body << type;
  body << command;
  body << jsonData;

  emit messageReady(JsonArray2Bytes(body));
}

void Router::notify(int type, const QString &command, const QString &jsonData) {
  QJsonArray body;
  body << -2; // requestId = -2 mean this is for notification
  body << type;
  body << command;
  body << jsonData;

  emit messageReady(JsonArray2Bytes(body));
}

int Router::getTimeout() const { return requestTimeout; }

// cancel last request from the sender
void Router::cancelRequest() {
#ifndef FK_CLIENT_ONLY
  replyMutex.lock();
  expectedReplyId = -1;
  replyTimeout = 0;
  extraReplyReadySemaphore = nullptr;
  replyMutex.unlock();

  if (replyReadySemaphore.available() > 0)
    replyReadySemaphore.acquire(replyReadySemaphore.available());
#endif
}

QString Router::waitForReply(int timeout) {
  QString ret;
#ifndef FK_CLIENT_ONLY
  replyReadySemaphore.tryAcquire(1, timeout * 1000);
  replyMutex.lock();
  ret = m_reply;
  replyMutex.unlock();
#endif
  return ret;
}

void Router::abortRequest() {
#ifndef FK_CLIENT_ONLY
  replyMutex.lock();
  if (expectedReplyId != -1) {
    replyReadySemaphore.release();
    if (extraReplyReadySemaphore)
      extraReplyReadySemaphore->release();
    expectedReplyId = -1;
    extraReplyReadySemaphore = nullptr;
  }
  replyMutex.unlock();
#endif
}

void Router::handlePacket(const QByteArray &rawPacket) {
#ifndef FK_CLIENT_ONLY
  static QMap<QString, void (*)(ServerPlayer *, const QString &)> lobby_actions;
  if (lobby_actions.size() <= 0) {
    lobby_actions["UpdateAvatar"] = [](ServerPlayer *sender,
                                       const QString &jsonData) {
      auto arr = String2Json(jsonData).array();
      auto avatar = arr[0].toString();

      if (CheckSqlString(avatar)) {
        auto sql = QString("UPDATE userinfo SET avatar='%1' WHERE id=%2;")
                       .arg(avatar)
                       .arg(sender->getId());
        ExecSQL(ServerInstance->getDatabase(), sql);
        sender->setAvatar(avatar);
        sender->doNotify("UpdateAvatar", avatar);
      }
    };
    lobby_actions["UpdatePassword"] = [](ServerPlayer *sender,
                                         const QString &jsonData) {
      auto arr = String2Json(jsonData).array();
      auto oldpw = arr[0].toString();
      auto newpw = arr[1].toString();
      auto sql_find =
          QString("SELECT password, salt FROM userinfo WHERE id=%1;")
              .arg(sender->getId());

      auto passed = false;
      auto arr2 = SelectFromDatabase(ServerInstance->getDatabase(), sql_find);
      auto result = arr2[0].toObject();
      passed = (result["password"].toString() ==
                QCryptographicHash::hash(
                    oldpw.append(result["salt"].toString()).toLatin1(),
                    QCryptographicHash::Sha256)
                    .toHex());
      if (passed) {
        auto sql_update =
            QString("UPDATE userinfo SET password='%1' WHERE id=%2;")
                .arg(QCryptographicHash::hash(
                         newpw.append(result["salt"].toString()).toLatin1(),
                         QCryptographicHash::Sha256)
                         .toHex())
                .arg(sender->getId());
        ExecSQL(ServerInstance->getDatabase(), sql_update);
      }

      sender->doNotify("UpdatePassword", passed ? "1" : "0");
    };
    lobby_actions["CreateRoom"] = [](ServerPlayer *sender,
                                     const QString &jsonData) {
      auto arr = String2Json(jsonData).array();
      auto name = arr[0].toString();
      auto capacity = arr[1].toInt();
      auto timeout = arr[2].toInt();
      auto settings =
          QJsonDocument(arr[3].toObject()).toJson(QJsonDocument::Compact);
      ServerInstance->createRoom(sender, name, capacity, timeout, settings);
    };
    lobby_actions["EnterRoom"] = [](ServerPlayer *sender,
                                    const QString &jsonData) {
      auto arr = String2Json(jsonData).array();
      auto roomId = arr[0].toInt();
      auto room = ServerInstance->findRoom(roomId);
      if (room) {
        auto settings = QJsonDocument::fromJson(room->getSettings());
        auto password = settings["password"].toString();
        if (password.isEmpty() || arr[1].toString() == password) {
          room->addPlayer(sender);
        } else {
          sender->doNotify("ErrorMsg", "room password error");
        }
      } else {
        sender->doNotify("ErrorMsg", "no such room");
      }
    };
    lobby_actions["ObserveRoom"] = [](ServerPlayer *sender,
                                      const QString &jsonData) {
      auto arr = String2Json(jsonData).array();
      auto roomId = arr[0].toInt();
      auto room = ServerInstance->findRoom(roomId);
      if (room) {
        auto settings = QJsonDocument::fromJson(room->getSettings());
        auto password = settings["password"].toString();
        if (password.isEmpty() || arr[1].toString() == password) {
          room->addObserver(sender);
        } else {
          sender->doNotify("ErrorMsg", "room password error");
        }
      } else {
        sender->doNotify("ErrorMsg", "no such room");
      }
    };
    lobby_actions["Chat"] = [](ServerPlayer *sender, const QString &jsonData) {
      sender->getRoom()->chat(sender, jsonData);
    };
    lobby_actions["RefreshRoomList"] = [](ServerPlayer *sender,
                                    const QString &jsonData) {
      ServerInstance->updateRoomList(sender);
    };
  }
#endif

  QJsonDocument packet = QJsonDocument::fromJson(rawPacket);
  if (packet.isNull() || !packet.isArray())
    return;

  int requestId = packet[0].toInt();
  int type = packet[1].toInt();
  QString command = packet[2].toString();
  QString jsonData = packet[3].toString();

  if (type & TYPE_NOTIFICATION) {
    if (type & DEST_CLIENT) {
#ifndef FK_SERVER_ONLY
      ClientInstance->callLua(command, jsonData, false);
#endif
    }
#ifndef FK_CLIENT_ONLY
    else {
      ServerPlayer *player = qobject_cast<ServerPlayer *>(parent());
      if (command == "Heartbeat") {
        player->alive = true;
        return;
      }

      Room *room = player->getRoom();
      if (room->isLobby() && lobby_actions.contains(command))
        lobby_actions[command](player, jsonData);
      else {
        if (command == "QuitRoom") {
          room->removePlayer(player);
        } else if (command == "AddRobot") {
          if (ServerInstance->getConfig("enableBots").toBool()) room->addRobot(player);
        } else if (command == "KickPlayer") {
          int i = jsonData.toInt();
          auto p = room->findPlayer(i);
          if (p && !room->isStarted()) room->removePlayer(p);
        } else if (command == "Ready") {
          player->setReady(!player->isReady());
          room->doBroadcastNotify(room->getPlayers(), "ReadyChanged",
              QString("[%1,%2]").arg(player->getId()).arg(player->isReady()));
        } else if (command == "StartGame") {
          room->manuallyStart();
        } else if (command == "Chat") {
          room->chat(player, jsonData);
        } else if (command == "PushRequest") {
          room->pushRequest(QString("%1,").arg(player->getId()) + jsonData);
        }
      }
    }
#endif
  } else if (type & TYPE_REQUEST) {
    this->requestId = requestId;
    this->requestTimeout = packet[4].toInt();

    if (type & DEST_CLIENT) {
#ifndef FK_SERVER_ONLY
      qobject_cast<Client *>(parent())->callLua(command, jsonData, true);
#endif
    } else {
      // requesting server is not allowed
      Q_ASSERT(false);
    }
  }
#ifndef FK_CLIENT_ONLY
  else if (type & TYPE_REPLY) {
    QMutexLocker locker(&replyMutex);

    ServerPlayer *player = qobject_cast<ServerPlayer *>(parent());
    player->setThinking(false);
    // qDebug() << "wake up!";
    auto room = player->getRoom();
    if (room->getThread()) {
      room->getThread()->wakeUp();
    }

    if (requestId != this->expectedReplyId)
      return;

    this->expectedReplyId = -1;

    if (replyTimeout >= 0 &&
        replyTimeout < requestStartTime.secsTo(QDateTime::currentDateTime()))
      return;

    m_reply = jsonData;
    // TODO: callback?

    replyReadySemaphore.release();
    if (extraReplyReadySemaphore) {
      extraReplyReadySemaphore->release();
      extraReplyReadySemaphore = nullptr;
    }

    locker.unlock();
    emit replyReady();
  }
#endif
}
